package com.skt.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.skt.domain.PO.UploadedFile;
import com.skt.domain.VO.FilePublicUrlVO;
import com.skt.domain.VO.FileUploadVO;
import com.skt.mapper.UploadedFileMapper;
import com.skt.security.LoginUser;
import com.skt.service.FileService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.time.LocalDateTime;
import java.util.Set;
import java.util.UUID;

/*
init()

→ 初始化上传根目录，确保本地存储路径存在且安全。
uploadImage(MultipartFile file, SecurityContext securityContext)

→ 主入口方法：接收图片上传，校验、保存文件并存入数据库，返回访问信息。
getPublicFileUrl(Long fileId)

→ （待实现）根据文件 ID 生成公开访问 URL（当前返回 null）。
validateFile(MultipartFile file)

→ 校验上传文件是否为空、超大或格式非法（仅允许 JPG/JPEG/PNG/GIF）。
getExtension(String filename)

→ 从原始文件名中提取并标准化为小写的扩展名（如 ".jpg"）。
createStoragePath(LocalDateTime now, String extension)

→ 按年/月/日生成唯一、安全的存储路径，防止路径遍历和文件名冲突。
sanitizeFilename(String filename)

→ 清理文件名中的非法字符，仅保留字母、数字、点、下划线和连字符。
saveFileToDisk(MultipartFile file, Path targetPath)

→ 将上传的文件内容写入磁盘指定路径。
buildFileRecord(Long userId, MultipartFile file, String originalFilename, String storagePath)

→ 构造数据库实体对象 UploadedFile，记录文件元数据。
buildFileUploadVO(UploadedFile record, LocalDateTime uploadedAt)

→ 封装返回给前端的上传结果（含 URL、ID、文件名、大小、时间等）。
getCurrentUserId(SecurityContext securityContext)

→ 从 Spring Security 上下文中安全提取当前登录用户的 ID。
 */

/**
 * 方法名	                是否接口方法	所属位置	        说明	        被谁调用
 * uploadImage(...)	        ✅ 是	    FileService     接口	        被 @Override 实现	外部（如 Controller）
 * getPublicFileUrl(...)	✅ 是	    FileService     接口	        被 @Override 实现	外部（如 Controller）
 * init()	                ❌ 否	    FileServiceImpl	被 @PostConstruct 标记，Spring 生命周期回调	Spring 容器（启动时自动调用）
 * validateFile(...)	    ❌ 否	    FileServiceImpl	私有工具方法	uploadImage
 * getExtension(...)	    ❌ 否	    FileServiceImpl	私有工具方法	uploadImage → validateFile
 * createStoragePath(...)	❌ 否	    FileServiceImpl	私有工具方法	uploadImage
 * sanitizeFilename(...)	❌ 否	    FileServiceImpl	私有工具方法	createStoragePath
 * saveFileToDisk(...)	    ❌ 否	    FileServiceImpl	私有工具方法	uploadImage
 * buildFileRecord(...)	    ❌ 否	    FileServiceImpl	私有工具方法	uploadImage
 * buildFileUploadVO(...)	❌ 否	    FileServiceImpl	私有工具方法	uploadImage
 * getCurrentUserId(...)	❌ 否	    FileServiceImpl	私有工具方法	uploadImage
 */

@Service
@RequiredArgsConstructor
public class FileServiceImpl extends ServiceImpl<UploadedFileMapper, UploadedFile> implements FileService {

    /**
     * 允许上传的图片文件扩展名集合（必须包含点号，且为小写）。
     * 支持格式：JPG、JPEG、PNG、GIF。
     */
    private static final Set<String> ALLOWED_EXTENSIONS = Set.of(".jpg", ".jpeg", ".png", ".gif");

    /**
     * 单个文件最大允许大小，单位为字节。
     * 默认限制为 5MB（5 * 1024 * 1024 = 5,242,880 字节）。
     */
    private static final long MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB

    /**
     * 文件上传的本地根目录路径，通过配置文件注入。
     * 默认值为 {@code ./uploads/images}，表示项目根目录下的 uploads/images 文件夹。
     * 示例配置：{@code app.upload.base-dir=/var/www/uploads}
     */
    @Value("${app.upload.base-dir:./uploads/images}")
    private String baseUploadDir;

    /**
     * 用于生成文件访问 URL 的前缀，通过配置文件注入。
     * 前端将通过该 URL 访问已上传的文件（通常由文件 ID 路由）。
     * 默认值为 {@code /api/files}，最终 URL 形如：{@code /api/files/123}
     */
    @Value("${app.upload.file-url-prefix:/api/files}")
    private String fileUrlPrefix;

    /**
     * 上传根目录的绝对路径（标准化后的 {@link Path} 对象）。
     * 在 {@link #init()} 方法中初始化，确保路径安全且存在。
     */
    private Path rootPath;

    /**
     * 初始化上传根目录。
     * <p>
     * 在 Spring 容器完成依赖注入后执行，将 {@link #baseUploadDir} 转换为标准化的绝对路径，
     * 并尝试创建该目录（若不存在）。若创建失败，抛出运行时异常。
     * </p>
     *
     * @throws RuntimeException 若无法创建上传根目录
     */
    @PostConstruct
    public void init() {
        this.rootPath = Paths.get(baseUploadDir).toAbsolutePath().normalize();
        try {
            Files.createDirectories(rootPath);
        } catch (IOException e) {
            throw new RuntimeException("无法创建上传根目录: " + rootPath, e);
        }
    }

    /**
     * 上传图片文件，并将其信息持久化到数据库。
     * <p>
     * 该方法为事务性操作，包含以下步骤：
     * <ol>
     *   <li>校验文件合法性（非空、大小、扩展名）</li>
     *   <li>获取当前登录用户 ID</li>
     *   <li>生成基于时间与随机字符串的安全存储路径</li>
     *   <li>将文件写入磁盘</li>
     *   <li>构建并保存 {@link UploadedFile} 数据库记录</li>
     *   <li>返回包含访问 URL 的响应对象</li>
     * </ol>
     * </p>
     *
     * @param file              待上传的图片文件（Multipart 格式）
     * @param securityContext   Spring Security 上下文，用于获取当前用户身份
     * @return 包含文件 ID、访问 URL、原始文件名、大小和上传时间的 {@link FileUploadVO}
     * @throws IllegalArgumentException 若文件不合法
     * @throws IllegalStateException    若用户未登录或安全上下文异常
     * @throws RuntimeException         若文件保存或目录创建失败
     */
    @Override
    @Transactional
    public FileUploadVO uploadImage(MultipartFile file, SecurityContext securityContext) {
        // 1. 校验文件：非空、大小不超过 5MB、扩展名合法（如 .jpg, .png 等）
        validateFile(file);

        // 2. 从 Spring Security 上下文中获取当前登录用户的 ID
        Long userId = getCurrentUserId(securityContext);

        // 3. 获取用户上传时的原始文件名（如 "avatar.JPG"）
        String originalFilename = file.getOriginalFilename();

        // 4. 从原始文件名中提取并标准化扩展名（转为小写，如 ".jpg"）
        String extension = getExtension(originalFilename);

        // 5. 记录当前上传时间，用于生成存储路径和响应数据
        LocalDateTime uploadTime = LocalDateTime.now();

        // 6. 根据上传时间（年/月/日）和扩展名，生成唯一的存储路径（如 ./uploads/2025/04/15/1712345678901_a1b2c3.jpg）
        Path targetPath = createStoragePath(uploadTime, extension);

        // 7. 将文件内容写入磁盘指定路径（自动覆盖同名文件）
        saveFileToDisk(file, targetPath);

        // 8. 计算相对于上传根目录的路径，并统一使用正斜杠 "/"（兼容 Windows 和 Linux）
        String relativePath = rootPath.relativize(targetPath).toString().replace("\\", "/");

        // 9. 构建数据库记录对象：包含用户 ID、文件大小、原始名、存储路径等信息
        UploadedFile record = buildFileRecord(userId, file, originalFilename, relativePath);

        // 10. 将文件记录持久化到数据库
        this.save(record);

        // 11. 构造并返回前端所需的文件上传响应对象（含访问 URL、ID、时间等）
        return buildFileUploadVO(record, uploadTime);
    }

    /**
     * 获取公开访问 URL
     * @param fileId 文件唯一标识
     * @return 包含文件 ID 和公开访问路径的 VO 对象
     * @throws IllegalArgumentException 若文件不存在
     */
    @Override
    public FilePublicUrlVO getPublicFileUrl(Long fileId) {
        UploadedFile file = this.getById(fileId);
        if (file == null) {
            return null; // 不抛异常，由 Controller 决定如何处理
        }

        // TODO: 后续可加权限校验、公开类型判断等

        String publicUrl = fileUrlPrefix + "/" + fileId;
        return new FilePublicUrlVO(publicUrl, fileId);
    }

    // ==================== 私有工具方法 ====================

    /**
     * 校验上传的文件是否合法。
     * <ul>
     *   <li>文件不能为空</li>
     *   <li>文件大小不能超过 5MB</li>
     *   <li>文件名不能为空或空白</li>
     *   <li>扩展名必须为 .jpg/.jpeg/.png/.gif（不区分大小写）</li>
     * </ul>
     *
     * @param file 待校验的 MultipartFile 对象
     * @throws IllegalArgumentException 若文件不符合要求
     */
    private void validateFile(MultipartFile file) {
        if (file == null || file.isEmpty()) {
            throw new IllegalArgumentException("上传文件不能为空");
        }
        if (file.getSize() > MAX_FILE_SIZE) {
            throw new IllegalArgumentException("文件大小不能超过 5MB");
        }

        // 获取用户上传文件时的原始文件名（包含扩展名），例如 "avatar.jpg"。
        String originalFilename = file.getOriginalFilename();
        if (originalFilename == null || originalFilename.isBlank()) {
            throw new IllegalArgumentException("文件名无效");
        }

        // 从原始文件名中提取并标准化文件扩展名（转为小写，保留点号），例如输入 "PHOTO.JPG" 会返回 ".jpg"。
        String ext = getExtension(originalFilename);
        if (!ALLOWED_EXTENSIONS.contains(ext)) {
            throw new IllegalArgumentException("仅支持图片格式: jpg, jpeg, png, gif");
        }
    }

    /**
     * 从原始文件名中提取扩展名（包括点号），并转换为小写。
     * 例如："Avatar.JPG" → ".jpg"
     *
     * @param filename 原始文件名
     * @return 小写的扩展名（如 ".png"）
     * @throws IllegalArgumentException 如果文件名不包含扩展名（即无 '.'）
     */
    private String getExtension(String filename) {
        // 找到文件名中 最后一个点号 . 的位置。
        int lastDot = filename.lastIndexOf('.');
        if (lastDot == -1) {
            throw new IllegalArgumentException("文件必须包含扩展名");
        }
        // 从最后一个点开始截取到末尾（比如 "JPG" → ".JPG"），然后转成小写（→ ".jpg"），确保后续判断时不区分大小写。
        return filename.substring(lastDot).toLowerCase();
    }

    /**
     * 根据当前时间生成安全的文件存储路径。
     * 路径格式：{baseDir}/{year}/{month}/{day}/{timestamp}_{12位随机字符串}{extension}
     * 同时确保路径不会逃逸出目标目录（防止路径遍历攻击）。
     *
     * @param now       上传时间（用于生成目录层级）
     * @param extension 文件扩展名（如 ".jpg"）
     * @return 生成的绝对文件路径
     * @throws RuntimeException 若创建目录失败
     * @throws IllegalArgumentException 若生成的文件名非法（路径逃逸）
     */
    private Path createStoragePath(LocalDateTime now, String extension) {
        // 获取当前时间的毫秒时间戳，用作文件名前缀以保证时间维度上的唯一性。
        String timestamp = String.valueOf(System.currentTimeMillis());
        // 生成一个12位的随机字符串（来自UUID），用于增强文件名的唯一性，防止冲突。
        String random = UUID.randomUUID().toString().replace("-", "").substring(0, 12);
        // 将时间戳、随机字符串和扩展名拼接成完整文件名，并清理其中的非法字符，确保文件名安全合法。
        String safeFilename = sanitizeFilename(timestamp + "_" + random + extension);

        String year = String.valueOf(now.getYear());
        // 将当前日期的“月份”格式化为两位数的字符串，不足两位时前面补零。
        String month = String.format("%02d", now.getMonthValue());
        String day = String.format("%02d", now.getDayOfMonth());

        Path dir = rootPath.resolve(year).resolve(month).resolve(day);
        try {
            Files.createDirectories(dir);
        } catch (IOException e) {
            throw new RuntimeException("创建存储目录失败: " + dir, e);
        }

        Path path = dir.resolve(safeFilename).normalize();
        if (!path.startsWith(dir)) {
            throw new IllegalArgumentException("非法文件名");
        }
        return path;
    }

    /**
     * 对文件名进行安全化处理，移除可能引起安全问题或系统兼容性问题的字符。
     * 仅保留字母、数字、点（.）、下划线（_）和连字符（-），其余字符替换为下划线。
     *
     * @param filename 原始文件名
     * @return 安全的文件名
     */
    private String sanitizeFilename(String filename) {
        return filename.replaceAll("[^a-zA-Z0-9._-]", "_");
    }

    /**
     * 将 MultipartFile 的内容写入指定的磁盘路径。
     *
     * @param file       要保存的文件对象
     * @param targetPath 目标存储路径（绝对路径）
     * @throws RuntimeException 若写入过程中发生 I/O 错误
     */
    private void saveFileToDisk(MultipartFile file, Path targetPath) {
        try (InputStream in = file.getInputStream()) {
            Files.copy(in, targetPath, StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException e) {
            throw new RuntimeException("文件保存失败: " + targetPath, e);
        }
    }

    /**
     * 构建用于持久化的 UploadedFile 数据库实体对象。
     * 设置用户 ID、原始文件名、存储路径、文件大小、MIME 类型、存储类型及业务类型。
     *
     * @param userId           当前操作用户 ID
     * @param file             上传的文件对象
     * @param originalFilename 原始文件名
     * @param storagePath      相对于上传根目录的存储路径（使用正斜杠）
     * @return 构建好的 UploadedFile 实体
     */
    private UploadedFile buildFileRecord(Long userId, MultipartFile file,
                                         String originalFilename, String storagePath) {
        UploadedFile record = new UploadedFile();
        record.setUserId(userId);
        record.setOriginalFilename(originalFilename);
        record.setStoragePath(storagePath);
        record.setFileSize(file.getSize());
        record.setMimeType(file.getContentType());
        record.setStorageType("local");
        record.setBusinessType("IMAGE"); // 明确标识为图片业务类型
        return record;
    }

    /**
     * 构建返回给前端的文件上传结果 VO 对象。
     * URL 由配置前缀与文件 ID 拼接而成，用于后续访问该文件。
     *
     * @param record      已保存的数据库记录
     * @param uploadedAt  文件上传时间
     * @return 包含 fileId、url、filename、size、uploadedAt 的 VO 对象
     */
    private FileUploadVO buildFileUploadVO(UploadedFile record, LocalDateTime uploadedAt) {
        return FileUploadVO.builder()
                .fileId(record.getId())
                .url(fileUrlPrefix + "/" + record.getId())
                .filename(record.getOriginalFilename())
                .size(record.getFileSize())
                .uploadedAt(uploadedAt)
                .build();
    }

    /**
     * 从 Spring Security 上下文中提取当前登录用户的用户 ID。
     *
     * @param securityContext Spring Security 上下文
     * @return 当前用户的 ID
     * @throws IllegalStateException 若上下文为空、未认证或主体类型不匹配
     */
    private Long getCurrentUserId(SecurityContext securityContext) {
        if (securityContext == null) {
            throw new IllegalStateException("安全上下文为空");
        }
        Authentication authentication = securityContext.getAuthentication();
        if (authentication == null || !authentication.isAuthenticated()) {
            throw new IllegalStateException("用户未登录，请先登录");
        }
        Object principal = authentication.getPrincipal();
        if (!(principal instanceof LoginUser)) {
            throw new IllegalStateException("认证主体类型错误，无法获取用户ID");
        }
        return ((LoginUser) principal).getUserId();
    }
}